package com.nisovin.magicspells.spells.instant; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Random; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.entity.Arrow; import org.bukkit.entity.Egg; import org.bukkit.entity.EnderPearl; import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.Item; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; import org.bukkit.entity.Projectile; import org.bukkit.entity.Snowball; import org.bukkit.entity.ThrownPotion; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.entity.CreatureSpawnEvent; import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason; import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.entity.PotionSplashEvent; import org.bukkit.event.entity.ProjectileHitEvent; import org.bukkit.event.player.PlayerPickupItemEvent; import org.bukkit.event.player.PlayerTeleportEvent; import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; import org.bukkit.inventory.ItemStack; import org.bukkit.metadata.FixedMetadataValue; import org.bukkit.util.Vector; import com.nisovin.magicspells.MagicSpells; import com.nisovin.magicspells.Subspell; import com.nisovin.magicspells.events.SpellTargetEvent; import com.nisovin.magicspells.spelleffects.EffectPosition; import com.nisovin.magicspells.spells.InstantSpell; import com.nisovin.magicspells.util.MagicConfig; import com.nisovin.magicspells.util.Util; public class ProjectileSpell extends InstantSpell { private Class<? extends Projectile> projectileClass; private ItemStack projectileItem; private double velocity; private double horizSpread; private double vertSpread; private boolean applySpellPowerToVelocity; private boolean requireHitEntity; private boolean cancelDamage; private boolean removeProjectile; private int maxDistanceSquared; private int effectInterval; private List<String> spellNames; private List<Subspell> spells; private int aoeRadius; private boolean targetPlayers; private boolean allowTargetChange; private String strHitCaster; private String strHitTarget; private HashMap<Projectile, ProjectileInfo> projectiles; private HashMap<Item, ProjectileInfo> itemProjectiles; private Random random = new Random(); public ProjectileSpell(MagicConfig config, String spellName) { super(config, spellName); String projectileType = getConfigString("projectile", "arrow"); if (projectileType.equalsIgnoreCase("arrow")) { projectileClass = Arrow.class; } else if (projectileType.equalsIgnoreCase("snowball")) { projectileClass = Snowball.class; } else if (projectileType.equalsIgnoreCase("egg")) { projectileClass = Egg.class; } else if (projectileType.equalsIgnoreCase("enderpearl")) { projectileClass = EnderPearl.class; } else if (projectileType.equalsIgnoreCase("potion")) { projectileClass = ThrownPotion.class; } else { ItemStack item = Util.getItemStackFromString(projectileType); if (item != null) { item.setAmount(0); projectileItem = item; } } if (projectileClass == null && projectileItem == null) { MagicSpells.error("Invalid projectile type on spell '" + internalName + "'"); } velocity = getConfigFloat("velocity", 0); horizSpread = getConfigFloat("horizontal-spread", 0); vertSpread = getConfigFloat("vertical-spread", 0); applySpellPowerToVelocity = getConfigBoolean("apply-spell-power-to-velocity", false); requireHitEntity = getConfigBoolean("require-hit-entity", false); cancelDamage = getConfigBoolean("cancel-damage", true); removeProjectile = getConfigBoolean("remove-projectile", true); maxDistanceSquared = getConfigInt("max-distance", 0); maxDistanceSquared = maxDistanceSquared * maxDistanceSquared; effectInterval = getConfigInt("effect-interval", 0); spellNames = getConfigStringList("spells", null); aoeRadius = getConfigInt("aoe-radius", 0); targetPlayers = getConfigBoolean("target-players", false); allowTargetChange = getConfigBoolean("allow-target-change", true); strHitCaster = getConfigString("str-hit-caster", ""); strHitTarget = getConfigString("str-hit-target", ""); if (projectileClass != null) { projectiles = new HashMap<Projectile, ProjectileInfo>(); } else if (projectileItem != null) { itemProjectiles = new HashMap<Item, ProjectileSpell.ProjectileInfo>(); } } @Override public void initialize() { super.initialize(); spells = new ArrayList<Subspell>(); if (spellNames != null) { for (String spellName : spellNames) { Subspell spell = new Subspell(spellName); if (spell.process()) { spells.add(spell); } else { MagicSpells.error("Projectile spell '" + internalName + "' attempted to add invalid spell '" + spellName + "'."); } } } if (spells.size() == 0) { MagicSpells.error("Projectile spell '" + internalName + "' has no spells!"); } if (projectileClass != null) { if (projectileClass == EnderPearl.class) { registerEvents(new EnderTpListener()); } else if (projectileClass == Egg.class) { registerEvents(new EggListener()); } else if (projectileClass == ThrownPotion.class) { registerEvents(new PotionListener()); } registerEvents(new ProjectileListener()); } else if (projectileItem != null) { registerEvents(new PickupListener()); } } @Override public PostCastAction castSpell(Player player, SpellCastState state, float power, String[] args) { if (state == SpellCastState.NORMAL) { if (projectileClass != null) { Projectile projectile = player.launchProjectile(projectileClass); projectile.setBounce(false); if (velocity > 0) { projectile.setVelocity(player.getLocation().getDirection().multiply(velocity)); } if (horizSpread > 0 || vertSpread > 0) { Vector v = projectile.getVelocity(); v.add(new Vector((random.nextDouble()-.5) * horizSpread, (random.nextDouble()-.5) * vertSpread, (random.nextDouble()-.5) * horizSpread)); projectile.setVelocity(v); } if (applySpellPowerToVelocity) { projectile.setVelocity(projectile.getVelocity().multiply(power)); } projectile.setMetadata("MagicSpellsSource", new FixedMetadataValue(MagicSpells.plugin, "ProjectileSpell_" + internalName)); projectiles.put(projectile, new ProjectileInfo(player, power, (effectInterval > 0 ? new RegularProjectileMonitor(projectile) : null))); playSpellEffects(EffectPosition.CASTER, projectile); } else if (projectileItem != null) { Item item = player.getWorld().dropItem(player.getEyeLocation(), projectileItem.clone()); Vector v = player.getLocation().getDirection().multiply(velocity > 0 ? velocity : 1); if (horizSpread > 0 || vertSpread > 0) { v.add(new Vector((random.nextDouble()-.5) * horizSpread, (random.nextDouble()-.5) * vertSpread, (random.nextDouble()-.5) * horizSpread)); } if (applySpellPowerToVelocity) { v.multiply(power); } item.setVelocity(v); item.setPickupDelay(10); itemProjectiles.put(item, new ProjectileInfo(player, power, new ItemProjectileMonitor(item))); playSpellEffects(EffectPosition.CASTER, item); } } return PostCastAction.HANDLE_NORMALLY; } public boolean projectileHitEntity(Entity projectile, LivingEntity target, ProjectileInfo info) { if (!info.done && (maxDistanceSquared == 0 || projectile.getLocation().distanceSquared(info.start) <= maxDistanceSquared)) { if (aoeRadius == 0) { float power = info.power; // check player if (!targetPlayers && target instanceof Player) return false; // call target event SpellTargetEvent evt = new SpellTargetEvent(this, info.player, target, power); Bukkit.getPluginManager().callEvent(evt); if (evt.isCancelled()) { return false; } else if (allowTargetChange) { target = evt.getTarget(); power = evt.getPower(); } // run spells for (Subspell spell : spells) { if (spell.isTargetedEntitySpell()) { spell.castAtEntity(info.player, target, power); playSpellEffects(EffectPosition.TARGET, target); } else if (spell.isTargetedLocationSpell()) { spell.castAtLocation(info.player, target.getLocation(), power); playSpellEffects(EffectPosition.TARGET, target.getLocation()); } } // send messages String entityName; if (target instanceof Player) { entityName = ((Player)target).getDisplayName(); } else { EntityType entityType = target.getType(); entityName = MagicSpells.getEntityNames().get(entityType); if (entityName == null) { entityName = entityType.name().toLowerCase(); } } sendMessage(info.player, strHitCaster, "%t", entityName); if (target instanceof Player) { sendMessage((Player)target, strHitTarget, "%a", info.player.getDisplayName()); } } else { aoe(projectile, info); } info.done = true; } return true; } private boolean projectileHitLocation(Entity projectile, ProjectileInfo info) { if (!requireHitEntity && !info.done && (maxDistanceSquared == 0 || projectile.getLocation().distanceSquared(info.start) <= maxDistanceSquared)) { if (aoeRadius == 0) { for (Subspell spell : spells) { if (spell.isTargetedLocationSpell()) { Location loc = projectile.getLocation(); Util.setLocationFacingFromVector(loc, projectile.getVelocity()); spell.castAtLocation(info.player, loc, info.power); playSpellEffects(EffectPosition.TARGET, loc); } } sendMessage(info.player, strHitCaster); } else { aoe(projectile, info); } info.done = true; } return true; } private void aoe(Entity projectile, ProjectileInfo info) { playSpellEffects(EffectPosition.SPECIAL, projectile.getLocation()); List<Entity> entities = projectile.getNearbyEntities(aoeRadius, aoeRadius, aoeRadius); for (Entity entity : entities) { if (entity instanceof LivingEntity && (targetPlayers || !(entity instanceof Player)) && !entity.equals(info.player)) { LivingEntity target = (LivingEntity)entity; float power = info.power; // call target event SpellTargetEvent evt = new SpellTargetEvent(this, info.player, target, power); Bukkit.getPluginManager().callEvent(evt); if (evt.isCancelled()) { continue; } else if (allowTargetChange) { target = evt.getTarget(); } power = evt.getPower(); // run spells for (Subspell spell : spells) { if (spell.isTargetedEntitySpell()) { spell.castAtEntity(info.player, target, power); playSpellEffects(EffectPosition.TARGET, target); } else if (spell.isTargetedLocationSpell()) { spell.castAtLocation(info.player, target.getLocation(), power); playSpellEffects(EffectPosition.TARGET, target.getLocation()); } } // send message if player if (target instanceof Player) { sendMessage((Player)target, strHitTarget, "%a", info.player.getDisplayName()); } } } sendMessage(info.player, strHitCaster); } public class ProjectileListener implements Listener { @EventHandler(priority=EventPriority.HIGHEST) public void onEntityDamage(EntityDamageByEntityEvent event) { if (!(event.getDamager() instanceof Projectile)) return; Projectile projectile = (Projectile)event.getDamager(); ProjectileInfo info = projectiles.get(projectile); if (info == null || event.isCancelled()) return; if (!(event.getEntity() instanceof LivingEntity)) { return; } projectileHitEntity(projectile, (LivingEntity)event.getEntity(), info); if (cancelDamage) { event.setCancelled(true); } if (info.monitor != null) { info.monitor.stop(); } } @EventHandler public void onProjectileHit(ProjectileHitEvent event) { final Projectile projectile = (Projectile)event.getEntity(); ProjectileInfo info = projectiles.get(projectile); if (info != null) { projectileHitLocation(projectile, info); // remove it from world if (removeProjectile) { projectile.remove(); } // remove it at end of tick Bukkit.getScheduler().scheduleSyncDelayedTask(MagicSpells.plugin, new Runnable() { public void run() { projectiles.remove(projectile); } }, 0); if (info.monitor != null) { info.monitor.stop(); } } } } public class EnderTpListener implements Listener { @EventHandler(ignoreCancelled=true) public void onPlayerTeleport(PlayerTeleportEvent event) { if (event.getCause() == TeleportCause.ENDER_PEARL) { for (Projectile projectile : projectiles.keySet()) { if (locationsEqual(projectile.getLocation(), event.getTo())) { event.setCancelled(true); return; } } } } } public class EggListener implements Listener { @EventHandler(ignoreCancelled=true) public void onCreatureSpawn(CreatureSpawnEvent event) { if (event.getSpawnReason() == SpawnReason.EGG) { for (Projectile projectile : projectiles.keySet()) { if (locationsEqual(projectile.getLocation(), event.getLocation())) { event.setCancelled(true); return; } } } } } public class PotionListener implements Listener { @EventHandler(ignoreCancelled=true) public void onPotionSplash(PotionSplashEvent event) { if (projectiles.containsKey(event.getPotion())) { event.setCancelled(true); } } } public class PickupListener implements Listener { @EventHandler(ignoreCancelled=true) public void onPickupItem(PlayerPickupItemEvent event) { Item item = event.getItem(); ProjectileInfo info = itemProjectiles.get(item); if (info != null) { event.setCancelled(true); projectileHitEntity(item, event.getPlayer(), info); item.remove(); itemProjectiles.remove(item); info.monitor.stop(); } } } private boolean locationsEqual(Location loc1, Location loc2) { return Math.abs(loc1.getX() - loc2.getX()) < 0.1 && Math.abs(loc1.getY() - loc2.getY()) < 0.1 && Math.abs(loc1.getZ() - loc2.getZ()) < 0.1; } private class ProjectileInfo { Player player; Location start; float power; boolean done; ProjectileMonitor monitor; public ProjectileInfo(Player player, float power) { this.player = player; this.start = player.getLocation().clone(); this.power = power; this.done = false; this.monitor = null; } public ProjectileInfo(Player player, float power, ProjectileMonitor monitor) { this(player, power); this.monitor = monitor; } } private interface ProjectileMonitor { public void stop(); } private class ItemProjectileMonitor implements Runnable, ProjectileMonitor { Item item; int taskId; int count; public ItemProjectileMonitor(Item item) { this.item = item; this.taskId = Bukkit.getScheduler().scheduleSyncRepeatingTask(MagicSpells.plugin, this, 1, 1); this.count = 0; } @Override public void run() { Vector v = item.getVelocity(); if (Math.abs(v.getY()) < .01 || (Math.abs(v.getX()) < .01 && Math.abs(v.getZ()) < .01)) { ProjectileInfo info = itemProjectiles.get(item); if (info != null) { projectileHitLocation(item, info); stop(); } } if (effectInterval > 0 && count % effectInterval == 0) { playSpellEffects(EffectPosition.SPECIAL, item.getLocation()); } if (++count > 300) { stop(); } } public void stop() { item.remove(); itemProjectiles.remove(item); Bukkit.getScheduler().cancelTask(taskId); } } private class RegularProjectileMonitor implements Runnable, ProjectileMonitor { Projectile projectile; Location prevLoc; int taskId; int count = 0; public RegularProjectileMonitor(Projectile projectile) { this.projectile = projectile; this.prevLoc = projectile.getLocation(); this.taskId = Bukkit.getScheduler().scheduleSyncRepeatingTask(MagicSpells.plugin, this, effectInterval, effectInterval); } @Override public void run() { playSpellEffects(EffectPosition.SPECIAL, prevLoc); prevLoc = projectile.getLocation(); if (!projectile.isValid() || projectile.isOnGround()) { stop(); } if (count++ > 100) { stop(); } } public void stop() { Bukkit.getScheduler().cancelTask(taskId); } } }